<?php
/**
* This file is part of Webappkit, a packages system for PHP web apps
* @link http://webappkit.net
* @package webappkit
* @subpackage storages
* @author J.Ducastel <jeremie@ducastel.name>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

/**
* Root class for webappkit object instances storages.
* generic filesystem-based storage.
* Use search patterns as path template to retrieve a file by search criteria,
* then build an object instance from the file's content.
* Search patterns may contain strings between double accolads, including an optional criteria name as below
* {optional-part{criteria}optional part}
* If criteria has a value, it will be replaced by its value surrounded by optional parts and accolads will be removed
* Else, the wole string will be removed from search pattern
* @author J.Ducastel <jeremie@ducastel.name>
* @todo criteria validation (regex ?)
*/
class WakStorage extends wakTool {
/*--------------------------------------------------------------------
 STATIC METHODS
--------------------------------------------------------------------*/
	/**
	 * factory pattern, use this method to build instances
	 * all subclasses have to implement this method
	 * because of PHP object model limitations
	 * @static
	 * @param string $kit_id parent kit id
	 * @param array $cfg configuration data, will be merged with defaults
	 * @return wakTool
	 */
	function & Factory($kit_id,$cfg) {
		return new WakStorage($kit_id,$cfg);
	}
/*--------------------------------------------------------------------
CONSTRUCTOR
--------------------------------------------------------------------*/

/*--------------------------------------------------------------------
PUBLIC INSTANCE METHODS
--------------------------------------------------------------------*/
	/**
	* search and returns a single item from store, if found
	* @param array|string $criteria can be either a single criterium ($id)
	* or an associative array
	* @access public
	* @return object
	*/
	function getItem($criteria) {
		//
		return $this->_getItem($criteria);
	}
	/**
	 * alias for getItem
	 */
	function get($criteria) {
		return $this->getItem($criteria);
	}

	/**
	 * search for an item
	 * @access public
	 * @param array $criteria
	 * @return bool
	 */
	function hasItem($criteria) {
		// filling criteria with defaults
		$criteria=$this->_fillCriteria($criteria); // print_r($criteria);
		// searching for file
		if ($this->_searchFile($criteria))
			return true;
		// trigger_error("has : no");
		return false;
	}
	/**
	 * alias for hasItem
	 */
	function has($criteria) {
		return $this->hasItem($criteria);
	}

	/**
	* sets new default value for a criteria
	* @param string $key
	* @param string $value
	* @access public
	* @return bool
	*/
	function setCriteria($key,$value) {
		$this->criteria[$key]=$value;
		return true;
	}/**/

	/**
	* get search pattern
	* @access public
	* @return string
	*/
	function getSearchPattern() {
		return $this->search;
	}

	/**
	* set search pattern
	* @access public
	* @param string $pattern
	* @return bool
	*/
	function setSearchPattern($pattern) {
		if (!is_string($pattern))
			return false;
		$this->search=$pattern;
		return true;
	}
/*--------------------------------------------------------------------
VARS / MEMBERS
--------------------------------------------------------------------*/
	/**
	 * @var array default configuration to be overrided
	 * [storage][search] => $this->search
	 * [storage][class] => $this->stored_class
	 * [criteria] => $this->criteria
	 */
	var $cfg=array(
		'storage'=>array(
			'search'=>'{{id}}.txt'
			,'class'=>'')
		,'criteria'=>array());

	/**
	 * base path to search within = kit root path
	 */
	var $path='';

	/**
	 * @var string search template
	 * criteria to replace have to be between accolads like
	 * {optional_part{criteria_name}optional_part}
	 **/
	var $search;

	/**
	 * @var string objects class name
	 **/
	var $stored_class;

	/**
	 * @var array default values for criteria
	 **/
	var $criteria=array();

	/**
	 * @var array object instances cache serialized criteria => instance
	 */
	var $objects=array();
/*--------------------------------------------------------------------
PRIVATE / PROTECTED METHODS
--------------------------------------------------------------------*/


	/**
	* storage intialization
	* @access protected
	* @return bool
	*/
	function _init() {
		// importing config
		if (isset($this->cfg['storage']['class']))
			$this->stored_class=$this->cfg['storage']['class'];
		if (isset($this->cfg['storage']['search']))
			$this->search=$this->cfg['storage']['search'];
		if (isset($this->cfg['criteria']))
			$this->criteria=$this->cfg['criteria'];
		// setting search path = kit root path
		$this->path=Webappkit::getpathFromId($this->kit);
		return true;
	}/**/

	/**
	 * search, get and builds and item from passed criteria
	 * @access protected
	 * @param array|string $criteria
	 * @return object|null
	 */
	function & _getItem($criteria) {
		// filling criteria with defaults
		$criteria=$this->_fillCriteria($criteria);
		// building unique instance id from criteria
		$id=serialize($criteria);
		// searching into objects cache, building if required
		if (!isset($this->objects[$id])) {
			// searching filename
			if (!$filename=$this->_searchFile($criteria)) {
				// trigger_error("item $id not found in storage {$this->name} from kit {$this->kit} (file $filename)",E_USER_NOTICE);
				$n=null; return $n;
			}
			// building
			$this->objects[$id]=&$this->_itemFactory($filename);
		}
		// returning instance
		return $this->objects[$id];
	}/**/

	/**
	 * computes unique string id from criteria.
	 * This is used as key for stored object references
	 * /
	 function _footPrint($criteria) {
		$str=join('',array_keys($criteria)).
	 }/**/

	/**
	 * Merge passed search criteria with defaults
	 * @access protected
	 * @param array $criteria associative array
	 * @return array
	 */
	function _fillCriteria($criteria) {
		// string criteria = id
		if (is_string($criteria)) {
			$criteria=array('id'=>$criteria);
		}
		// looping defaults
		foreach ($this->criteria as $key => $value) {
			// setting default if not set in arg
			if (!isset($criteria[$key]))
				$criteria[$key]=$value;
		}
		return $criteria;
	}/**/

	/**
	 * injects criteria into search template to get filename.
	 * Replace var's key by their actual value within search template.
	 * Scheme is {optional part before{key}optional part after}
	 * checks that file exists.
	 * @access protected
	 * @param array $criteria
	 * @return string filename or false if not found
	 */
	function _searchFile($criteria) {
		$n=0;
		$regex='\{([^{}]*)\{([a-zA-Z][a-zA-Z0-9_\.-]*)\}([^{}]*)\}';
		// echo $regex;
		$filename=$this->search;
		while (ereg($regex,$filename,$found)) {
			// avoiding infinite loop
			$n++; if ($n>50) break;
			// print_r($found);
			$before=$found[1];
			$key=$found[2];
			//$force=$found[3];
			$after=$found[3];
			// building replacement block
			if (isset($criteria[$key])
			and is_string($criteria[$key])
			and $criteria[$key]) {
				// provided, not empty string value
				$replace=$before.$criteria[$key].$after;
			} else {
				// not provided or null value
				$replace='';
			}
			// replacing
			// escaping regex special chars
			$before=str_replace('(','\(',$before);
			$before=str_replace(')','\)',$before);
			$after=str_replace('(','\(',$after);
			$after=str_replace(')','\)',$after);
			// replacing regex
			$ereg2='\{'.$before.'\{'.$key.'\}'.$after.'\}';
			$filename=ereg_replace($ereg2,$replace,$filename);
		}
		// adding kit path
		$filename=$this->path.$filename;
		// echo "<p>filename=$filename</p>";
		if (!file_exists($filename))
			return false;
		return $filename;
	}/**/

	/**
	* builds an object instance from the file name.
	* Passes file's content as param to constructor.
	* @param string $filename
	* @access protected
	* @return object
	*/
	function & _itemFactory($filename) {
		// reading file content
		$content=join('',file($filename));
		// getting class
		$class=$this->stored_class;
		// passing file content to constructor
		return new $class($content);
	}/**/
}
?>